本系列文已出版成書「NestJS 基礎必學實務指南:使用強大且易擴展的 Node.js 框架打造網頁應用程式」,感謝 iT 邦幫忙與博碩文化的協助。如果對 NestJS 有興趣、覺得這個系列文對你有幫助的話,歡迎前往購書,你的支持是我最大的寫作動力!
中文名稱為攔截器,受到 剖面導向程式設計 (Aspect Oriented Programming) 的啟發,為原功能的擴展邏輯,其特點如下:
Interceptor 可以透過 CLI 產生:
$ nest generate interceptor <INTERCEPTOR_NAME>
注意:
<INTERCEPTOR_NAME>
可以含有路徑,如:interceptors/hello-world
,這樣就會在src
資料夾下建立該路徑並含有 Interceptor。
這邊我建立一個 HelloWorldInterceptor
在 interceptors
資料夾下:
$ nest generate interceptor interceptors/hello-world
在 src
底下會看見一個名為 interceptors
的資料夾,裡面有 hello-world.interceptor.ts
以及 hello-world.interceptor.spec.ts
:
建立出來的 Interceptor 骨架如下,會發現 Interceptor 其實也是帶有 @Injectable
裝飾器的 class
,不過它必須實作 NestInterceptor
介面,並設計 intercept(context: ExecutionContext, next: CallHandler)
方法:
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable()
export class HelloWorldInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle();
}
}
CallHandler
為 Interceptor 的重要成員,它實作了 handle()
來調用路由處理的方法,進而導入對應的 Controller 方法,也就是說,如果在 Interceptor 不回傳 CallHandler
的 handle()
,將會使路由處理失去運作。
由於 CallHandler
為 intercept
方法的參數,故其一定是在 intercept
中被呼叫,也就是說,可以在回傳 handle()
之前 寫一段邏輯,使其可以在進入 Controller 的方法前被執行,又因為 handle()
回傳的是 Observable
,故可以透過 pipe
的方式 對回傳值做調整,使其可以在 Controller 的方法執行之後處理其他邏輯。
注意:
handle()
是Observable
,我們把它作為intercept
的回傳值是希望 Nest 可以去subscribe
它,根據Observable
的特性,若沒有去subscribe
它則不會執行其內部邏輯,這也是為什麼不回傳handle()
的話將會使路由處理失去運作的原因。
ExecutionContext
是繼承 ArgumentsHost
的 class
,其提供了更多關於此請求的相關訊息,下方為它提供的兩個方法,透過這兩個方法可以大幅提升應用的靈活性:
透過 getClass()
取得當前請求對應的 Controller Class:
const Controller: TodoController = context.getClass<TodoController>();
透過 getHandler()
取得當前請求對應的 Controller method,假設當前請求會呼叫 TodoController
的 getAll()
,那就會回傳 getAll
這個函式:
const method: Function = context.getHandler();
在使用之前,先將 hello-world.interceptor.ts
修改一下,在進入 Interceptor 時印出 Hello World!
並使用變數儲存進入的時間,再透過 tap
印出結束的時間與進入的時間差:
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class HelloWorldInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
console.log('Hello World!');
const input = Date.now();
const handler = next.handle();
return handler.pipe(
tap(() => console.log(`${ Date.now() - input } ms`))
);
}
}
修改完以後就來使用此 Interceptor,透過 @UseInterceptors
裝飾器即可輕鬆套用,使用的方式大致上可以分成兩種:
@UseInterceptors
裝飾器,只會針對該資源套用。@UseInterceptors
裝飾器,會針對整個 Controller 中的資源套用。下方以套用在 Controller 為例,修改 app.controller.ts
:
import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { AppService } from './app.service';
import { HelloWorldInterceptor } from './interceptors/hello-world.interceptor';
@Controller()
@UseInterceptors(HelloWorldInterceptor)
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}
透過瀏覽器查看 http://localhost:3000 會發現終端機出現了下方結果:
Hello World!
3 ms
如果設計了一個共用的 Interceptor 要套用在所有資源上的話,只需要修改 main.ts
即可,透過 useGlobalInterceptors
來配置全域 Interceptor:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { HelloWorldInterceptor } from './interceptors/hello-world.interceptor';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new HelloWorldInterceptor());
await app.listen(3000);
}
bootstrap();
上面的方法是透過模組外部完成全域配置的,與 Pipe 一樣可以用依賴注入的方式,透過指定 Provider 的 token
為 APP_INTERCEPTOR
來實現,這裡是用 useClass 來指定要建立實例的類別:
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { HelloWorldInterceptor } from './interceptors/hello-world.interceptor';
@Module({
imports: [],
controllers: [AppController],
providers: [
AppService,
{
provide: APP_INTERCEPTOR,
useClass: HelloWorldInterceptor
}
]
})
export class AppModule {}
Interceptor 可以在不修改 Controller 的情況下去擴充邏輯,是十分方便的功能。這裡附上今天的懶人包:
CallHandler
為重要成員,需要呼叫其 handle()
來讓路由機制得以運行。ExecutionContext
提供了 getClass()
與 getHandler()
來提升靈活性。CallHandler 為 Interceptor 的重要成員,它實作了 handle() 來調用路由處理的方法,進而導入對應的 Controller 方法,也就是說,如果在 Interceptor 不調用 CallHandler 的 handle(),將會使路由處理失去運作。
您好,根据文中示例,尝试了一下intercept
返回不是CallHandler.handle()及其rxjs/operators
处理结果的情况,发现也會使路由處理失去運作。
如果在 Interceptor 不調用 CallHandler 的 handle(),將會使路由處理失去運作。
改为"如果在 Interceptor 不調用 CallHandler 的 handle()并将其结果返回,將會使路由處理失去運作。"是不是更精确些呢
你好,不好意思,我說明的不夠精確,handle()
基本上就是呼叫 Controller
對應的 handler,並以 Observable
的形式讓我們可以用 RxJS
的技巧來針對 handler 處理完的資料做進一步的處理,如果返回值不是 handle()
所串聯的事件流,那 NestJS 就無法 subscribe
到它,而 Observable
沒有被 subscribe
的情況下是不會被執行的,就會造成所謂「路由處理失去運作」的情況。
我有更新文章進行補充了,感謝你的建議!
看到中間突然冒出個RxJS的東西卡住了好久不懂,去官往翻了一下還是不懂observable的概念...
https://rxjs-dev.firebaseapp.com/guide/observable
你好,Observable
是一種可觀察的物件,是 RxJS 裡面的核心角色之一,透過訂閱的方式來觀察這個物件提供的資料。
詳細的內容可以參考 Mike 大大的文章,基本上 Observable
是觀察者模式(Observable Pattern)的實作,當然,在 RxJS 的世界裡,有很多種可觀察的物件,在這篇文章中有非常詳細的說明。
謝謝您 我了解一下
感謝您直接提供參考文章,比起在網路上搜索大海撈針真的省很多時間